/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.engine.access;

import axil.api.AxilObject;
import axil.api.Context;
import axil.api.access.Accessor;
import axil.engine.AxilContext;
import axil.engine.Config.Option;
import axil.engine.access.discovery.Discovery;
import axil.framework.type.AxilShell;
import axil.stdlib.core.type.Nil;

import static axil.framework.Functions.*;


/**
 * A class representing a chain of accessor objects. The chain is followed each
 * time a host application object is accessed, navigating through the path to
 * extract an inner member.
 */
public class AccessPath {
	private String[] parts;
	private Accessor[] path;


	public AccessPath(String[] parts) {
		this.parts = parts;
		this.path = null;
	}


	public String root() {
		return parts[0];
	}


	private AxilObject discover(Context context) {
		Accessor root = new ContextAccessor(parts[0]);
		Object value = root.access(context, null);
		if (value == null) {
			/*
			 * In this case, the host application has passed a null host
			 * application object. This is likely a defect in the host
			 * application itself, not in Axil or the script. We let the host
			 * application decide how to handle this.
			 */
			if (enabled(Option.nulls)) {
				return Nil.object;
			} else {
				throw error(axil(), "null-context-value", nv("path", this));
			}
		}
		return discover(root, context, value);
	}


	private AxilObject discover(Object value) {
		if ((value == null) || (value == Nil.object)) {
			return Nil.object;
		}
		if (value instanceof AxilShell) {
			value = ((AxilShell)value).intrinsic();
		}
		return discover(new ImmediateAccessor(), AxilContext.empty, value);
	}


	private AxilObject discover(Accessor first, Context context, Object value) {
		final int size = parts.length;
		Accessor[] list = new Accessor[size];
		list[0] = first;
		Discovery type = Discovery.matching(value);
		if (type == null) {
			if (enabled(Option.trace)) {
				throw error(axil(), "invalid-path-element",
				            nv("path", this), nv("part", parts[0]));
			}
			return Nil.object;
		}
		for (int i = 1; i < size; i ++) {
			final String part = parts[i];
			Accessor a = type.accessor(context, value, part);
			if (a == null) {
				if (enabled(Option.trace)) {
					throw error(axil(), "invalid-path-element",
					            nv("path", this), nv("part", part));
				}
				return Nil.object;
			}
			list[i] = a;
			value = a.access(context, value);
			if (value == null) {
				return Nil.object;
			}
			if (i < (size - 1)) {
				type = Discovery.matching(value);
				if (type == null) {
					if (enabled(Option.trace)) {
						throw error(axil(), "cannot-traverse",
						            nv("path", this), nv("part", part));
					}
					return Nil.object;
				}
			}
		}
		this.path = list;
		return axil(value);
	}


	/**
	 * Evaluate this node using the information contained in the current
	 * context. Always returns a non-null node.
	 */
	public AxilObject eval(Context context) {
		if (path == null) {
			return discover(context);
		}
		Object value = null;
		for (Accessor e : path) {
			value = e.access(context, value);
			if (value == null) {
				return Nil.object;
			}
		}
		return axil(value);
	}


	/**
	 * Evaluate the path, but start from the given object instead of looking
	 * up the root value in the context.
	 */
	public AxilObject eval(AxilObject root) {
		if (path == null) {
			return discover(root);
		}
		Object value = root;
		final Context context = AxilContext.empty;
		for (Accessor e : path) {
			value = e.access(context, value);
			if (value == null) {
				return Nil.object;
			}
		}
		return axil(value);
	}


	/**
	 * Return a string representation of this object. The string returned is
	 * never null.
	 */
	public String toString() {
		StringBuilder b = new StringBuilder();
		for (String p : parts) {
			if (b.length() > 0) {
				b.append('.');
			}
			b.append(p);
		}
		return b.toString();
	}
}
